package com.vst.orm.search;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vst.orm.util.InternalUtil;
import com.vst.orm.util.MetadataUtil;
import com.vst.orm.util.SearchUtil;

public abstract class BaseSearchProcessor {
	private static Logger logger = LoggerFactory.getLogger(BaseSearchProcessor.class);
	protected static int QLTYPE_HQL = 0;
	protected static int QLTYPE_EQL = 1;
	protected int qlType;
	protected MetadataUtil metadataUtil;
	protected String rootAlias = "_it";
	protected static final String ROOT_PATH = "";

	protected BaseSearchProcessor(int qlType, MetadataUtil metaDataUtil) {
		if (metaDataUtil == null) {
			throw new IllegalArgumentException("A SearchProcessor cannot be initialized with a null MetadataUtil.");
		}
		this.qlType = qlType;
		this.metadataUtil = metaDataUtil;
	}

	public MetadataUtil getMetadataUtil() {
		return this.metadataUtil;
	}

	public void setRootAlias(String alias) {
		this.rootAlias = alias;
	}

	public String generateQL(Class<?> entityClass, ISearch search, List<Object> paramList) {
		if (entityClass == null) {
			throw new NullPointerException("The entity class for a search cannot be null");
		}
		SearchContext ctx = new SearchContext(entityClass, this.rootAlias, paramList);

		List<Field> fields = checkAndCleanFields(search.getFields());

		String select = generateSelectClause(ctx, fields, search.isDistinct());
		String where = generateWhereClause(ctx, checkAndCleanFilters(search.getFilters()), search.isDisjunction());
		String orderBy = generateOrderByClause(ctx, checkAndCleanSorts(search.getSorts()));
		applyFetches(ctx, checkAndCleanFetches(search.getFetches()), fields);
		String from = generateFromClause(ctx, true);

		StringBuilder sb = new StringBuilder();
		sb.append(select);
		sb.append(from);
		sb.append(where);
		sb.append(orderBy);

		String query = sb.toString();
		if (logger.isDebugEnabled()) {
			logger.debug("generateQL:\n  " + query);
		}
		return query;
	}

	public String generateRowCountQL(Class<?> entityClass, ISearch search, List<Object> paramList) {
		if (entityClass == null) {
			throw new NullPointerException("The entity class for a search cannot be null");
		}
		SearchContext ctx = new SearchContext(entityClass, this.rootAlias, paramList);

		String where = generateWhereClause(ctx, checkAndCleanFilters(search.getFilters()), search.isDisjunction());
		String from = generateFromClause(ctx, false);

		boolean useOperator = false;
		boolean notUseOperator = false;
		List<Field> fields = search.getFields();
		if (fields != null) {
			for (Field field : fields) {
				switch (field.getOperator()) {
				case 1:
				case 2:
				case 3:
				case 4:
				case 5:
				case 6:
					useOperator = true;
					break;
				default:
					notUseOperator = true;
				}
			}
		}
		if ((useOperator) && (notUseOperator)) {
			throw new Error("A search can not have a mix of fields with operators and fields without operators.");
		}
		if (useOperator) {
			return null;
		}
		StringBuilder sb = new StringBuilder();
		if (!search.isDistinct()) {
			sb.append("select count(*)");
		} else if (fields.size() == 0) {
			sb.append("select count(distinct ");
			sb.append(this.rootAlias).append(".id)");
		} else if (fields.size() == 1) {
			sb.append("select count(distinct ");
			String prop = ((Field) fields.get(0)).getProperty();
			if ((prop == null) || ("".equals(prop))) {
				sb.append(ctx.getRootAlias());
			} else {
				sb.append(getPathRef(ctx, prop));
			}
			sb.append(")");
		} else {
			throw new IllegalArgumentException(
					"Unfortunately, Hibernate Generic DAO does not currently support the count operation on a search that has distinct set with multiple fields.");
		}
		sb.append(from);
		sb.append(where);

		String query = sb.toString();
		if (logger.isDebugEnabled()) {
			logger.debug("generateRowCountQL:\n  " + query);
		}
		return query;
	}

	protected String generateSelectClause(SearchContext ctx, List<Field> fields, boolean distinct) {
		StringBuilder sb = null;
		boolean useOperator = false;
		boolean notUseOperator = false;
		boolean first = true;
		if (fields != null) {
			for (Field field : fields) {
				if (first) {
					sb = new StringBuilder("select ");
					if (distinct) {
						sb.append("distinct ");
					}
					first = false;
				} else {
					sb.append(", ");
				}
				String prop;
				if ((field.getProperty() == null) || ("".equals(field.getProperty()))) {
					prop = ctx.getRootAlias();
				} else {
					prop = getPathRef(ctx, field.getProperty());
				}
				switch (field.getOperator()) {
				case 6:
					sb.append("avg(");
					useOperator = true;
					break;
				case 1:
					sb.append("count(");
					useOperator = true;
					break;
				case 2:
					sb.append("count(distinct ");
					useOperator = true;
					break;
				case 3:
					sb.append("max(");
					useOperator = true;
					break;
				case 4:
					sb.append("min(");
					useOperator = true;
					break;
				case 5:
					sb.append("sum(");
					useOperator = true;
					break;
				default:
					notUseOperator = true;
				}
				sb.append(prop);
				if (useOperator) {
					sb.append(")");
				}
			}
		}
		if (first) {
			if (distinct) {
				return "select distinct " + ctx.getRootAlias();
			}
			return "select " + ctx.getRootAlias();
		}
		if ((useOperator) && (notUseOperator)) {
			throw new Error("A search can not have a mix of fields with operators and fields without operators.");
		}
		return sb.toString();
	}

	protected void applyFetches(SearchContext ctx, List<String> fetches, List<Field> fields) {
		List<String> fieldProps;
		if (fetches != null) {
			boolean hasFetches = false;
			boolean hasFields = false;
			for (String fetch : fetches) {
				getAlias(ctx, fetch, true);
				hasFetches = true;
			}
			if ((hasFetches) && (fields != null)) {
				fieldProps = new ArrayList();
				for (Field field : fields) {
					if (field.getOperator() == 0) {
						fieldProps.add(field.getProperty() + ".");
					}
					hasFields = true;
				}
				if (hasFields) {
					for (AliasNode node : ctx.aliases.values()) {
						if (node.fetch) {
							boolean hasAncestor = false;
							for (String field : fieldProps) {
								if (node.getFullPath().startsWith(field)) {
									hasAncestor = true;
									break;
								}
							}
							if (!hasAncestor) {
								node.fetch = false;
							}
						}
					}
				}
			}
		}
	}

	protected String generateFromClause(SearchContext ctx, boolean doEagerFetching) {
		StringBuilder sb = new StringBuilder(" from ");
		sb.append(ctx.rootClass.getName());
		sb.append(" ");
		sb.append(ctx.getRootAlias());
		sb.append(generateJoins(ctx, doEagerFetching));
		return sb.toString();
	}

	protected String generateJoins(SearchContext ctx, boolean doEagerFetching) {
		StringBuilder sb = new StringBuilder();

		Queue<AliasNode> queue = new LinkedList();
		queue.offer(ctx.aliases.get(""));
		while (!queue.isEmpty()) {
			AliasNode node = (AliasNode) queue.poll();
			if (node.parent != null) {
				sb.append(" left join ");
				if ((doEagerFetching) && (node.fetch)) {
					sb.append("fetch ");
				}
				sb.append(node.parent.alias);
				sb.append(".");
				sb.append(node.property);
				sb.append(" as ");
				sb.append(node.alias);
			}
			for (AliasNode child : node.children) {
				queue.offer(child);
			}
		}
		return sb.toString();
	}

	protected String generateOrderByClause(SearchContext ctx, List<Sort> sorts) {
		if (sorts == null) {
			return "";
		}
		StringBuilder sb = null;
		boolean first = true;
		for (Sort sort : sorts) {
			if (first) {
				sb = new StringBuilder(" order by ");
				first = false;
			} else {
				sb.append(", ");
			}
			if ((sort.isIgnoreCase()) && (this.metadataUtil.get(ctx.rootClass, sort.getProperty()).isString())) {
				sb.append("lower(");
				sb.append(getPathRef(ctx, sort.getProperty()));
				sb.append(")");
			} else {
				sb.append(getPathRef(ctx, sort.getProperty()));
			}
			sb.append(sort.isDesc() ? " desc" : " asc");
		}
		if (first) {
			return "";
		}
		return sb.toString();
	}

	protected String generateWhereClause(SearchContext ctx, List<Filter> filters, boolean isDisjunction) {
		String content = null;
		if ((filters == null) || (filters.size() == 0)) {
			return "";
		}
		if (filters.size() == 1) {
			content = filterToQL(ctx, (Filter) filters.get(0));
		} else {
			Filter junction = new Filter(null, filters, isDisjunction ? 101 : 100);
			content = filterToQL(ctx, junction);
		}
		return " where " + content;
	}

	protected String filterToQL(SearchContext ctx, Filter filter) {
		String property = filter.getProperty();
		Object value = filter.getValue();
		int operator = filter.getOperator();
		if ((value == null) && (!filter.isTakesNoValue())) {
			return null;
		}
		if ((operator == 8) || (operator == 9)) {
			if (((value instanceof Collection)) && (((Collection) value).size() == 0)) {
				return operator == 8 ? "1 = 2" : "1 = 1";
			}
			if (((value instanceof Object[])) && (((Object[]) value).length == 0)) {
				return operator == 8 ? "1 = 2" : "1 = 1";
			}
		}
		if (filter.isTakesListOfValues()) {
			value = prepareValue(ctx.rootClass, property, value, true);
		} else if (filter.isTakesSingleValue()) {
			value = prepareValue(ctx.rootClass, property, value, false);
		}
		Metadata metadata;
		switch (operator) {
		case 10:
			return getPathRef(ctx, property) + " is null";
		case 11:
			return getPathRef(ctx, property) + " is not null";
		case 8:
			return getPathRef(ctx, property) + " in (" + param(ctx, value) + ")";
		case 9:
			return getPathRef(ctx, property) + " not in (" + param(ctx, value) + ")";
		case 0:
			return getPathRef(ctx, property) + " = " + param(ctx, value);
		case 1:
			return getPathRef(ctx, property) + " != " + param(ctx, value);
		case 3:
			return getPathRef(ctx, property) + " > " + param(ctx, value);
		case 2:
			return getPathRef(ctx, property) + " < " + param(ctx, value);
		case 5:
			return getPathRef(ctx, property) + " >= " + param(ctx, value);
		case 4:
			return getPathRef(ctx, property) + " <= " + param(ctx, value);
		case 6:
			return getPathRef(ctx, property) + " like " + param(ctx, value.toString());
		case 7:
			return "lower(" + getPathRef(ctx, property) + ") like lower(" + param(ctx, value.toString()) + ")";
		case 100:
		case 101:
			if (!(value instanceof List)) {
				return null;
			}
			String op = filter.getOperator() == 100 ? " and " : " or ";

			StringBuilder sb = new StringBuilder("(");
			boolean first = true;
			for (Object o : (List) value) {
				if ((o instanceof Filter)) {
					String filterStr = filterToQL(ctx, (Filter) o);
					if (filterStr != null) {
						if (first) {
							first = false;
						} else {
							sb.append(op);
						}
						sb.append(filterStr);
					}
				}
			}
			if (first) {
				return null;
			}
			sb.append(")");
			return sb.toString();
		case 102:
			if (!(value instanceof Filter)) {
				return null;
			}
			String filterStr = filterToQL(ctx, (Filter) value);
			if (filterStr == null) {
				return null;
			}
			return "not " + filterStr;
		case 12:
			metadata = this.metadataUtil.get(ctx.rootClass, property);
			if (metadata.isCollection()) {
				return "not exists elements(" + getPathRef(ctx, property) + ")";
			}
			if (metadata.isString()) {
				String pathRef = getPathRef(ctx, property);
				return "(" + pathRef + " is null or " + pathRef + " = '')";
			}
			return getPathRef(ctx, property) + " is null";
		case 13:
			metadata = this.metadataUtil.get(ctx.rootClass, property);
			if (metadata.isCollection()) {
				return "exists elements(" + getPathRef(ctx, property) + ")";
			}
			if (metadata.isString()) {
				String pathRef = getPathRef(ctx, property);
				return "(" + pathRef + " is not null and " + pathRef + " != '')";
			}
			return getPathRef(ctx, property) + " is not null";
		case 200:
			if (!(value instanceof Filter)) {
				return null;
			}
			if ((value instanceof Filter)) {
				String simple = generateSimpleAllOrSome(ctx, property, (Filter) value, "some");
				if (simple != null) {
					return simple;
				}
				return "exists " + generateSubquery(ctx, property, (Filter) value);
			}
		case 201:
			if (!(value instanceof Filter)) {
				return null;
			}
			if ((value instanceof Filter)) {
				String simple = generateSimpleAllOrSome(ctx, property, (Filter) value, "all");
				if (simple != null) {
					return simple;
				}
				return "not exists " + generateSubquery(ctx, property, negate((Filter) value));
			}
		case 202:
			if (!(value instanceof Filter)) {
				return null;
			}
			if ((value instanceof Filter)) {
				String simple = generateSimpleAllOrSome(ctx, property, (Filter) value, "all");
				if (simple != null) {
					return "not ( " + simple + " )";
				}
				return "not exists " + generateSubquery(ctx, property, (Filter) value);
			}
			break;
		}
		throw new IllegalArgumentException("Filter comparison ( " + operator + " ) is invalid.");
	}

	protected String generateSubquery(SearchContext ctx, String property, Filter filter) {
		SearchContext ctx2 = new SearchContext();
		ctx2.rootClass = this.metadataUtil.get(ctx.rootClass, property).getJavaClass();
		ctx2.setRootAlias(this.rootAlias + ctx.nextSubqueryNum++);
		ctx2.paramList = ctx.paramList;
		ctx2.nextAliasNum = ctx.nextAliasNum;
		ctx2.nextSubqueryNum = ctx.nextSubqueryNum;

		List<Filter> filters = new ArrayList(1);
		filters.add(filter);
		String where = generateWhereClause(ctx2, filters, false);
		String joins = generateJoins(ctx2, false);
		ctx.nextAliasNum = ctx2.nextAliasNum;
		ctx.nextSubqueryNum = ctx2.nextSubqueryNum;

		StringBuilder sb = new StringBuilder();
		sb.append("(from ");
		sb.append(getPathRef(ctx, property));
		sb.append(" ");
		sb.append(ctx2.getRootAlias());
		sb.append(joins);
		sb.append(where);
		sb.append(")");

		return sb.toString();
	}

	protected String generateSimpleAllOrSome(SearchContext ctx, String property, Filter filter, String operation) {
		if ((filter.getProperty() != null) && (!filter.getProperty().equals(""))) {
			return null;
		}
		String op;
		switch (filter.getOperator()) {
		case 0:
			op = " = ";
			break;
		case 1:
			op = " != ";
			break;
		case 2:
			op = " > ";
			break;
		case 4:
			op = " >= ";
			break;
		case 3:
			op = " < ";
			break;
		case 5:
			op = " <= ";
			break;
		default:
			return null;
		}
		Object value = InternalUtil.convertIfNeeded(filter.getValue(),
				this.metadataUtil.get(ctx.rootClass, property).getJavaClass());

		return param(ctx, value) + op + operation + " elements(" + getPathRef(ctx, property) + ")";
	}

	protected Object prepareValue(Class<?> rootClass, String property, Object value, boolean isCollection) {
		if (value == null) {
			return null;
		}
		Class<?> expectedClass;
		if ((property != null) && (("class".equals(property)) || (property.endsWith(".class")))) {
			expectedClass = Class.class;
		} else {
			if ((property != null) && (("size".equals(property)) || (property.endsWith(".size")))) {
				expectedClass = Integer.class;
			} else {
				expectedClass = this.metadataUtil.get(rootClass, property).getJavaClass();
			}
		}
		if (isCollection) {
			Object[] val2;
			int i;
			if ((value instanceof Collection)) {
				val2 = new Object[((Collection) value).size()];
				i = 0;
				for (Object item : (Collection) value) {
					val2[(i++)] = InternalUtil.convertIfNeeded(item, expectedClass);
				}
			} else {
				val2 = new Object[((Object[]) value).length];
				i = 0;
				for (Object item : (Object[]) value) {
					val2[(i++)] = InternalUtil.convertIfNeeded(item, expectedClass);
				}
			}
			return val2;
		}
		return InternalUtil.convertIfNeeded(value, expectedClass);
	}

	protected Filter negate(Filter filter) {
		return Filter.not(addExplicitNullChecks(filter));
	}

	protected Filter addExplicitNullChecks(Filter filter) {
		return SearchUtil.walkFilter(filter, new SearchUtil.FilterVisitor() {
			public Filter visitAfter(Filter filter) {
				if ((filter.isTakesSingleValue()) || (filter.isTakesListOfValues())) {
					return Filter.and(new Filter[] { filter, Filter.isNotNull(filter.getProperty()) });
				}
				return filter;
			}
		}, false);
	}

	protected String param(SearchContext ctx, Object value) {
		if ((value instanceof Class)) {
			return ((Class) value).getName();
		}
		if ((value instanceof Collection)) {
			StringBuilder sb = new StringBuilder();
			boolean first = true;
			for (Object o : (Collection) value) {
				if (first) {
					first = false;
				} else {
					sb.append(",");
				}
				ctx.paramList.add(o);
				sb.append(":p");
				sb.append(Integer.toString(ctx.paramList.size()));
			}
			return sb.toString();
		}
		if ((value instanceof Object[])) {
			StringBuilder sb = new StringBuilder();
			boolean first = true;
			for (Object o : (Object[]) value) {
				if (first) {
					first = false;
				} else {
					sb.append(",");
				}
				ctx.paramList.add(o);
				sb.append(":p");
				sb.append(Integer.toString(ctx.paramList.size()));
			}
			return sb.toString();
		}
		ctx.paramList.add(value);
		return ":p" + Integer.toString(ctx.paramList.size());
	}

	protected String getPathRef(SearchContext ctx, String path) {
		if ((path == null) || ("".equals(path))) {
			return ctx.getRootAlias();
		}
		String[] parts = splitPath(ctx, path);

		return getAlias(ctx, parts[0], false).alias + "." + parts[1];
	}

	protected String[] splitPath(SearchContext ctx, String path) {
		if ((path == null) || ("".equals(path))) {
			return null;
		}
		int pos = path.lastIndexOf('.');
		if (pos == -1) {
			return new String[] { "", path };
		}
		String lastSegment = path.substring(pos + 1);
		String currentPath = path;
		boolean first = true;
		for (;;) {
			if (this.metadataUtil.isId(ctx.rootClass, currentPath)) {
				if (pos == -1) {
					return new String[] { "", path };
				}
				pos = currentPath.lastIndexOf('.', pos - 1);
			} else if ((!first) && (this.metadataUtil.get(ctx.rootClass, currentPath).isEntity())) {
				return new String[] { currentPath, path.substring(currentPath.length() + 1) };
			}
			first = false;
			if ((pos != -1) && (lastSegment.equals("size"))
					&& (this.metadataUtil.get(ctx.rootClass, currentPath.substring(0, pos)).isCollection())) {
				first = true;
			}
			if (pos == -1) {
				return new String[] { "", path };
			}
			currentPath = currentPath.substring(0, pos);
			pos = currentPath.lastIndexOf('.');
			if (pos == -1) {
				lastSegment = currentPath;
			} else {
				lastSegment = currentPath.substring(pos + 1);
			}
		}
	}

	protected AliasNode getAlias(SearchContext ctx, String path, boolean setFetch) {
		if ((path == null) || (path.equals(""))) {
			return (AliasNode) ctx.aliases.get("");
		}
		if (ctx.aliases.containsKey(path)) {
			AliasNode node = (AliasNode) ctx.aliases.get(path);
			if (setFetch) {
				while (node.parent != null) {
					node.fetch = true;
					node = node.parent;
				}
			}
			return node;
		}
		String[] parts = splitPath(ctx, path);

		int pos = parts[1].lastIndexOf('.');

		String alias = "a" + ctx.nextAliasNum++ + "_" + (pos == -1 ? parts[1] : parts[1].substring(pos + 1));

		AliasNode node = new AliasNode(parts[1], alias);

		getAlias(ctx, parts[0], setFetch).addChild(node);

		node.fetch = setFetch;
		ctx.aliases.put(path, node);

		return node;
	}

	protected static final class AliasNode {
		String property;
		String alias;
		boolean fetch;
		AliasNode parent;
		List<AliasNode> children = new ArrayList();

		AliasNode(String property, String alias) {
			this.property = property;
			this.alias = alias;
		}

		void addChild(AliasNode node) {
			this.children.add(node);
			node.parent = this;
		}

		public String getFullPath() {
			if (this.parent == null) {
				return "";
			}
			if (this.parent.parent == null) {
				return this.property;
			}
			return this.parent.getFullPath() + "." + this.property;
		}
	}

	protected static final class SearchContext {
		Class<?> rootClass;
		Map<String, BaseSearchProcessor.AliasNode> aliases = new HashMap();
		List<Object> paramList;
		int nextAliasNum = 1;
		int nextSubqueryNum = 1;

		public SearchContext() {
		}

		public SearchContext(Class<?> rootClass, String rootAlias, List<Object> paramList) {
			this.rootClass = rootClass;
			setRootAlias(rootAlias);
			this.paramList = paramList;
		}

		public void setRootAlias(String rootAlias) {
			this.aliases.put("", new BaseSearchProcessor.AliasNode("", rootAlias));
		}

		public String getRootAlias() {
			return ((BaseSearchProcessor.AliasNode) this.aliases.get("")).alias;
		}
	}

	protected List<Field> checkAndCleanFields(List<Field> fields) {
		if (fields == null) {
			return null;
		}
		for (Field field : fields) {
			if (field == null) {
				throw new IllegalArgumentException("The search contains a null field.");
			}
			if (field.getProperty() != null) {
				securityCheckProperty(field.getProperty());
			}
		}
		return fields;
	}

	protected List<String> checkAndCleanFetches(List<String> fetches) {
		return SearchUtil.walkList(fetches, new SearchUtil.ItemVisitor() {
			public String visit(String fetch) {
				BaseSearchProcessor.this.securityCheckProperty(fetch);
				return fetch;
			}
		}, true);
	}

	protected List<Sort> checkAndCleanSorts(List<Sort> sorts) {
		return SearchUtil.walkList(sorts, new SearchUtil.ItemVisitor() {
			public Sort visit(Sort sort) {
				BaseSearchProcessor.this.securityCheckProperty(sort.getProperty());
				return sort;
			}
		}, true);
	}

	protected List<Filter> checkAndCleanFilters(List<Filter> filters) {
		return SearchUtil.walkFilters(filters, new SearchUtil.FilterVisitor() {
			public Filter visitBefore(Filter filter) {
				if ((filter != null) && (filter.getValue() != null)) {
					if (filter.isTakesListOfSubFilters()) {
						if ((filter.getValue() instanceof List)) {
							for (Object o : (List) filter.getValue()) {
								if (!(o instanceof Filter)) {
									throw new IllegalArgumentException("The search has a filter (" + filter
											+ ") for which the value should be a List of Filters but there is an element in the list that is of type: "
											+ o.getClass());
								}
							}
						} else {
							throw new IllegalArgumentException("The search has a filter (" + filter
									+ ") for which the value should be a List of Filters but is not a list. The actual type is "
									+ filter.getValue().getClass());
						}
					} else if (filter.isTakesSingleSubFilter()) {
						if (!(filter.getValue() instanceof Filter)) {
							throw new IllegalArgumentException("The search has a filter (" + filter
									+ ") for which the value should be of type Filter but is of type: "
									+ filter.getValue().getClass());
						}
					} else if (filter.isTakesListOfValues()) {
						if ((!(filter.getValue() instanceof Collection))
								&& (!(filter.getValue() instanceof Object[]))) {
							throw new IllegalArgumentException("The search has a filter (" + filter
									+ ") for which the value should be a collection or array but is of type: "
									+ filter.getValue().getClass());
						}
					}
				}
				return filter;
			}

			public Filter visitAfter(Filter filter) {
				if (filter == null) {
					return null;
				}
				if (!filter.isTakesNoProperty()) {
					BaseSearchProcessor.this.securityCheckProperty(filter.getProperty());
				}
				if (filter.isTakesSingleSubFilter()) {
					if (filter.getValue() == null) {
						return null;
					}
				} else if (filter.isTakesListOfSubFilters()) {
					if (filter.getValue() == null) {
						return null;
					}
					List<Filter> list = (List) filter.getValue();
					if (list.size() == 0) {
						return null;
					}
					if (list.size() == 1) {
						return (Filter) list.get(0);
					}
				}
				return filter;
			}
		}, true);
	}

	protected Pattern INJECTION_CHECK = Pattern.compile("^[\\w\\.]*$");

	protected void securityCheckProperty(String property) {
		if (property == null) {
			return;
		}
		if (!this.INJECTION_CHECK.matcher(property).matches()) {
			throw new IllegalArgumentException(
					"A property used in a Search may only contain word characters (alphabetic, numberic and underscore \"_\") and dot \".\" seperators. This constraint was violated: "
							+ property);
		}
	}

	private static final ExampleOptions defaultExampleOptions = new ExampleOptions();

	public Filter getFilterFromExample(Object example) {
		return getFilterFromExample(example, null);
	}

	public Filter getFilterFromExample(Object example, ExampleOptions options) {
		if (example == null) {
			return null;
		}
		if (options == null) {
			options = defaultExampleOptions;
		}
		List<Filter> filters = new ArrayList();
		LinkedList<String> path = new LinkedList();
		Metadata metadata = this.metadataUtil.get(example.getClass());
		getFilterFromExampleRecursive(example, metadata, options, path, filters);
		if (filters.size() == 0) {
			return null;
		}
		if (filters.size() == 1) {
			return (Filter) filters.get(0);
		}
		return new Filter("AND", filters, 100);
	}

	private void getFilterFromExampleRecursive(Object example, Metadata metaData, ExampleOptions options,
			LinkedList<String> path, List<Filter> filters) {
		if ((metaData.isEntity()) && (!metaData.getIdType().isEmeddable())) {
			Object id = metaData.getIdValue(example);
			if (id != null) {
				filters.add(Filter.equal(listToPath(path, "id"), id));
				return;
			}
		}
		for (String property : metaData.getProperties()) {
			if ((options.getExcludeProps() == null) || (options.getExcludeProps().size() == 0)
					|| (!options.getExcludeProps().contains(listToPath(path, property)))) {
				Metadata pMetaData = metaData.getPropertyType(property);
				if (!pMetaData.isCollection()) {
					Object value = metaData.getPropertyValue(example, property);
					if (value == null) {
						if (!options.isExcludeNulls()) {
							filters.add(Filter.isNull(listToPath(path, property)));
						}
					} else if ((!options.isExcludeZeros()) || (!(value instanceof Number))
							|| (((Number) value).longValue() != 0L)) {
						if ((pMetaData.isEntity()) || (pMetaData.isEmeddable())) {
							path.add(property);
							getFilterFromExampleRecursive(value, pMetaData, options, path, filters);
							path.removeLast();
						} else if ((pMetaData.isString())
								&& ((options.getLikeMode() != 0) || (options.isIgnoreCase()))) {
							String val = value.toString();
							switch (options.getLikeMode()) {
							case 1:
								val = val + "%";
								break;
							case 2:
								val = "%" + val;
								break;
							case 3:
								val = "%" + val + "%";
							}
							filters.add(new Filter(listToPath(path, property), val, options.isIgnoreCase() ? 7 : 6));
						} else {
							filters.add(Filter.equal(listToPath(path, property), value));
						}
					}
				}
			}
		}
	}

	private String listToPath(List<String> list, String lastProperty) {
		StringBuilder sb = new StringBuilder();
		for (String prop : list) {
			sb.append(prop).append(".");
		}
		sb.append(lastProperty);
		return sb.toString();
	}
}
